from queue import Queue
from threading import Thread, Lock
import time
import random

DURATION = 0.2
mutex4print = Lock()


def download(item, name):
    with mutex4print:
        print(f'D-{name} Downloading ({item})')
    time.sleep(random.random() * DURATION * 1)
    with mutex4print:
        print(f'D-{name} Downloaded ({item})')
    return f'D-{name} Downloaded ({item})'


def resize(item, name):
    with mutex4print:
        print(f'R-{name} Resizing ({item})')
    time.sleep(random.random() * DURATION * 3)
    with mutex4print:
        print(f'R-{name} Resized ({item})')
    return f'R-{name} Resized ({item})'


def upload(item, name):
    with mutex4print:
        print(f'U-{name} Uploading ({item})')
    time.sleep(random.random() * DURATION * 1)
    with mutex4print:
        print(f'U-{name} Uploaded ({item})')
    return f'U-{name} Uploaded ({item})'


class ClosableQueue(Queue):

    SENTINEL = object()

    def __init__(self, name):
        super().__init__()
        self.name = name

    def close(self):
        self.put(self.SENTINEL)

    def __iter__(self):
        while True:
            item = self.get()
            try:
                if item is self.SENTINEL:
                    with mutex4print:
                        print(f'<<<< {self.name} encounters SENTINEL.')
                    return
                yield item
            finally:
                with mutex4print:
                    print(f']] Finally {self.name} task_done() for {item}')
                self.task_done()


class StoppableWorker(Thread):

    def __init__(self, func, in_queue, out_queue, name):
        super().__init__()
        self.func = func
        self.in_queue = in_queue
        self.out_queue = out_queue
        self.name = f'{name}'

    def run(self):
        for item in self.in_queue:
            result = self.func(item, self.name)
            self.out_queue.put(result)


def start_threads(count, *args):
    threads = [StoppableWorker(*args, i) for i in range(count)]
    for thread in threads:
        thread.start()
    return threads


def stop_thread(closable_queue, threads):
    for _ in range(len(threads)):
        closable_queue.close()
    closable_queue.join()
    for thread in threads:
        thread.join()


if '__main__' == __name__:

    def _main():
        download_queue = ClosableQueue('DOWNLOAD')
        resize_queue = ClosableQueue('RESIZE')
        upload_queue = ClosableQueue('UPLOAD')
        done_queue = ClosableQueue('DONE')
        download_threads = start_threads(3, download, download_queue, resize_queue)
        resize_threads = start_threads(4, resize, resize_queue, upload_queue)
        upload_threads = start_threads(5, upload, upload_queue, done_queue)

        N = 6
        for i in range(N):
            n = i + 1
            download_queue.put(f'obj-{n:04d}')

        stop_thread(download_queue, download_threads)
        stop_thread(resize_queue, resize_threads)
        stop_thread(upload_queue, upload_threads)

        with mutex4print:
            print(f'{done_queue.qsize()} items uploaded.')

        with mutex4print:
            print('All over')

    _main()
